<?php

/*
 * Define some constants
 */

define('authen_var', 'authenticated');
define('authen_error', 'authentication_error');
define('authen_custom_callback', false);

define('authen_http', false);
define('authen_http_type', 'Basic'); // alt: Digest
define('authen_http_qop', 'auth'); // alt: auth-int (not supported)
define('authen_http_realm', 'Restricted');
define('authen_http_not_req', false);
define('authen_http_require_role', false); // not supported, require user role (guest,user,admin,dev)

define('uuid_cookie', 'rem');
define('persist_cookie', 'token');
define('session_timeout', 1800);
define('persist_timeout', 2592000);


/*
 * IAuthenticationProvider
 */
interface IAuthenticationProvider {
	public static function GetUserByUniqueID($uuid);
	public static function GetUserByUsername($username);
	public static function GetUserByEmail($email);

	public function SetPassword($password);

	public function UserHasRole($role);
	public function UserHasPermission($permission);
}


/*
 * ISessionManager
 */
interface ISessionManager {
	public function SetPersistentSession();
}


/*
 * Auth
 */
class Auth {

	const CLEAR = 0;
	const INVALID = 1;
	const TIMED_OUT = 2;
	const INCOMPLETE = 3;
	const SECRET_EXPIRED = 4;
	const NO_CLEARTEXT = 5;

	private static $user;
	private static $secret;
	private static $secretExpires;
	private static $ip;
	private static $sessionID;
	private static $persistentID;
	private static $isPersistent;
	private static $loginResult;


	/*
	 * User()
	 */
	public static function User() {
		if (isset(self::$user)) {
			return self::$user;
		} else {
			if (isset($_SESSION['user']) && $_SESSION['user'] instanceof DBRecordParent) {
				$user = $_SESSION['user'];
			} else if (isset($_SESSION['userid'])) {
				$user = DB::Table('User')->Where('UserID', EQ, $_SESSION['userid'])->Single();
			} else {
				$user = DB::Null();
			}
			return self::$user = $_SESSION['user'] = $user;
		}
	}


	/*
	 * GetUser()
	 */
	public static function GetUser() {
		return self::User();
	}


	/*
	 * CheckAuth()
	 */
	public static function CheckAuth() {
		global $config;

		if (self::IsAuth()) {
			fb('IsAuth == true');
			$user = self::GetUser();
			if ($_SESSION['expires'] >= time()) {
				fb('Expired == false');
				if (self::MatchIP()) {
					fb('IP Match == true');
					// user is still logged in, session is valid, advance the session expiration time
					if ($_SESSION['persistent'] === true) {
						fb('Persistent Session');
						$_SESSION['expires'] = time() + persist_timeout;
						// check to make sure the persistent session cookies are still set
						if (!isset($_SESSION[uuid_cookie]) || !isset($_SESSION[persist_cookie])) {
							setcookie(uuid_cookie, $user->UUID, $_SESSION['expires'], $config['relpath']);
							setcookie(persist_cookie, $_SESSION['persistent_id'], $_SESSION['expires'], $config['relpath']);
						}
					} else {
						$_SESSION['expires'] = time() + session_timeout;		// advance expiration time
					}
				} else {
					fb('IP Match == false');
					// ip doesn't match
					$_SESSION[authen_var] = false;
					// set login result? do we warn about this or does it not justify notification?
					// $_SESSION['login_result'] = self::CLEAR;
				}
			} else {
				fb('Expired == true');
				// timed out
				$_SESSION[authen_var] = false;
				$_SESSION['login_result'] = self::TIMED_OUT;
				if (self::WWWAuthenticate() === false) {
					// redirect to login page
					redirect($config['base_href'] . $config['login']['url']);
				}
				exit();
			}
		} else {
			// $_SESSION[ authen_var ] was false (in case $_SESSION[ authen_var ] is not set, set it false)
			$_SESSION[authen_var] = false;

			fb('Not logged in.');

			// not logged in, check for a persistent session
			if (isset($_COOKIE[persist_cookie]) && isset($_COOKIE[uuid_cookie])) {
				if (authen_custom_callback !== false) {

				} else {
					fb('Checking for persistent session');
					$sessions = DB::Table('Session')->Where('UUID', EQ, $_COOKIE[uuid_cookie])->Where('PersistentID', EQ, $_COOKIE[persist_cookie]);
					$session = $sessions->Single();

					if (!DB::IsNull($session)) {
						if ($session->Persistent == true && $session->Expires >= time() && IP::FromInteger($session->IPAddress) == $_SERVER['REMOTE_ADDR']) {
							fb('Valid persistent connection');
							// user has a valid persistent session, set session vars for reauthentication
							$_SESSION[authen_var] = true;
							$_SESSION['persistent'] = true;
							$_SESSION['ip'] = IP::FromInteger($results->IPAddress);
							$_SESSION['expires'] = time() + persist_timeout;
							$_SESSION['persistent_id'] = $result->PersistentID;

							$_SESSION['username'] = $user->Username;
							$_SESSION['userid'] = $user->UserID;
						} else {
							fb('No valid persistent connection');
						}
					} else {
						fb('No persistent connection');
					}

					if (!$_SESSION[authen_var]) {
						setcookie(persist_cookie, '', time() - 3600, $config['relpath']);
					}
				}
			}

			if (authen_http == true && authen_http_not_req == false) {
				fb('Asking for www authentication');
				self::WWWAuthenticate();
				if (!self::IsAuth()) {
					emulate_404();
				}
			}
		}

		return self::IsAuth();
	}


	/*
	 * WWWAuthenticate()
	 */
	public static function WWWAuthenticate() {
		global $config;

		if (authen_http == true && authen_http_not_req == false) {
			if (isset($_SERVER['PHP_AUTH_USER']) || !empty($_SERVER['PHP_AUTH_DIGEST'])) {
				if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
					$digest = self::HttpDigestParse($_SERVER['PHP_AUTH_DIGEST']);
					$username = $data['username'];
				} else {
					$username = $_SERVER['PHP_AUTH_USER'];
				}

				if (!DB::IsNull($user = DB::Table('User')->Where('Username', EQ, $username)->Single())) {
					if (strtolower( authen_http_type ) == 'basic' && isset($_SERVER['PHP_AUTH_PW'])) {
						$client_provided = $_SERVER['PHP_AUTH_PW'];
						$server_generated = $user->Password;
					} else if (strtolower(authen_http_type) == 'digest' && $digest != false) {
						$A1 = md5(sprintf('%s:%s:%s:%s', authen_http_realm, $user->Password, $digest['username']));
						$A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $digest['uri']);
						$server_generated = md5($A1 . ':' . $digest['nonce'] . ':' . $digest['nc']
							. ':' . $digest['cnonce'] . ':' . $digest['qop'] . ':' . $A2);
						$client_provided = $digest['response'];
					}

					if (strlen($client_provided ) > 0 && $client_provided == $server_generated) {
						if (!(isset($config['login']['require_role']) && !self::UserHasRole($user, $config['login']['require_role']))) {
							$_SESSION[authen_var] = true;
							$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
							$_SESSION['expires'] = time() + session_timeout;
							$_SESSION['username'] = $user->Username;
							$_SESSION['userid'] = $user->UserID;

							setcookie(uuid_cookie, $user->UUID, time() + persist_timeout, $config['relpath']);

							DB::Table('Session')->Where('UUID', EQ, $user->UUID)->Delete();

							unset($_SERVER['PHP_AUTH_PW'], $_SERVER['PHP_AUTH_DIGEST']);
						}
					}
				}
			}

			if (!self::IsAuth()) {
				if (strtolower(authen_http_type) == 'digest') {
					$_SESSION['digest'] = array(
						'nonce' => $nonce = uniqid(mt_rand(), true),
						'opaque' => $opaque = md5(authen_http_realm)
					);

					header(sprintf('WWW-Authenticate: Digest realm="%s",qop="%s",nonce="%s",opaque="%s"', authen_http_realm, authen_http_qop, $nonce, $opaque));
				} else {
					header(sprintf('WWW-Authenticate: Basic realm="%s"', authen_http_realm));
				}

				header("HTTP/1.1 401 Unauthorized");

				echo "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n" .
					"<html><head>\n" .
					"<title>401 Authorization Required</title>\n" .
					"</head><body>\n" .
					"<h1>Authorization Required</h1>\n" .
					"<p>This server could not verify that you\n" .
					"are authorized to access the document\n" .
					"requested.  Either you supplied the wrong\n" .
					"credentials (e.g., bad password), or your\n" .
					"browser doesn't understand how to supply\n" .
					"the credentials required.</p>\n" .
					"<hr>\n" .
					$_SERVER[ 'SERVER_SIGNATURE' ] .
					"\n</body></html>";
				exit();
			}
		} else {
			return false;
		}
	}


	/*
	 * ProcLogin()
	 * @DEPRECATED
	 */
	public static function ProcLogin() {
		//__deprecated(__FILE__, __FUNCTION__, __LINE__);
		if ($_POST['do'] == 'login') {
			self::Login();
		} else if ($_REQUEST['do'] == 'logout') {
			self::Logout();
		}
	}


	/*
	 * Login()
	 */
	public static function Login($args = null) {
		global $config;

		// if user is already logged in, return
		if (self::IsAuth()) {
			return;
		}

		if (is_null($args)) {
			$args = $_POST;
		}

		// ensure login request is complete, contains a username, and one of cleartext pass, hash or hmac
		if (authen_http && $_SERVER['PHP_AUTH_USER']) {
			$_SESSION['username'] = $_SERVER['PHP_AUTH_USER'];
		} else if (!empty($args['user']) && (!isset($args['pass']) || !isset($args['hash']) || !isset($args['hmac']))) {
			$_SESSION['username'] = $args['user'];
			$_SESSION['persistent'] = ($args['persistent'] == 'true');
		} else {
			fb('Login request incomplete');
			// login request incomplete
			$_SESSION['login_result'] = self::INCOMPLETE;
			return;
		}

		$response = DB::Table('User')->Where('Username', EQ, $_SESSION['username']);

		if ($response->Count() != 1) {
			fb('Invalid login: ' . count($response) . ' matches for user ' . $_SESSION['username']);
			// no account, or multiple accounts with this user id exist
			self::SetLoginResult(self::INVALID);

			// log this incident
			//logger(

			return false;
		} else {
			$user = $response->Single();
			fb('Found user');

			if (isset($config['login']['require_role']) && !self::UserHasRole($user, $config['login']['require_role'])) {
				return false;
			}

			// Verify password hmac
			if ($args['hmac']) {
				fb('Using HMAC');
				if (self::ValidateSecret()) {
					fb('Valid secret');
					$client_provided = strtolower($args['hmac']);
					$server_generated = hash_hmac('md5', $user->Password, self::GetSecret());
					self::DestroySecret();
				} else {
					fb('Invalid secret');
					self::SetLoginResult(self::SECRET_EXPIRED);
					return false;
				}

			// Verify password hash
			} else if ($args['hash']) {
				fb('Using hash');
				$client_provided = strtolower($args['hash']);
				fb('Calculating hash with salt ' . self::GetSalt());
				fb('Calculating hash with password ' . $user->Password);
				$server_generated = sha1($user->Password . self::GetSalt());

			// Verify cleartext password
			} else if($args['pass']) {
				fb('Using cleartext');
				if($config['login']['allow_cleartext']) {
					$client_provided = $args['pass'];
					$server_generated = $user->Password;
				} else {
					fb('Cleartext not permitted');
					self::SetLoginResult(self::NO_CLEARTEXT);
					return false;
				}
			}

			fb('ClientProvided: ' . $client_provided);
			fb('ServerGenerated: ' . $server_generated);

			if ($client_provided == $server_generated) {
				$_SESSION[authen_var] = true;
				$_SESSION['expires'] = time() + session_timeout;
				$_SESSION['userid'] = $user->UserID;
				$_SESSION['username'] = $user->Username;
				self::SetIP();

				setcookie(uuid_cookie, $user->UUID, time() + persist_timeout, $config['relpath']);

				DB::Table('Session')->Where('UUID', EQ, $user->UUID)->Delete();

				if ($_SESSION['persistent']) {
					$_SESSION['expires'] = time() + persist_timeout;

					setcookie(persist_cookie, $_SESSION['persistent_id'], $_SESSION['expires'], $config['relpath']);

					$session = DB::Table('Session')->NewRow(array(
						'UUID' => $user->UUID,
						'IPAddress' => self::$ip,
						'Expires' => $_SESSION['expires'], //Session::$expires,
						'Persistent' => true,
						'PersistentID' => self::GetPersistentID(true)
					));

					$session->Save();
				}

				self::SetLoginResult(self::CLEAR);

				unset($esc_user, $_SESSION['pass'], $_SESSION['hash'], $_SESSION['hmac']);

				if ($args['post_from']) {
					redirect($config['base_href'] . $args['post_from']);
					exit();
				}

				return true;
			} else {
				// invalid login
				self::SetLoginResult(self::INVALID);
				self::DestroySecret();
				self::GetSecret(true);

				return false;
			}
		}
	}


	/*
	 * Logout()
	 */
	public static function Logout() {
		if ($_SESSION[authen_var] == true) {
			$_SESSION[authen_var] = false;

			// remove record of persistent session
			if (self::$isPersistent) {
				DB::Table('Session')->Where('UUID', EQ, self::$user->UUID)->Delete();
				setcookie(persist_cookie, '', time() - 3600, $config['relpath']);
			}

			session_regenerate_id(true);
			self::GetSecret(true);
		}
	}


	/*
	 * LastUsername()
	 */
	public static function LastUsername() {
		if ($_REQUEST['user']) {
			return $_REQUEST['user'];
		} else if (isset($_REQUEST[uuid_cookie])) {
			// $_COOKIE[] could also be used, but $_REQUEST allows the UUID to be included by GET or POST also
			$user = DB::Table('User')->Where('UUID', EQ, $_REQUEST[uuid_cookie])->Single();
			return $user !== false ? $user->Username : '';
		}
	}


	/*
	 * UserHasRole()
	 */
	public static function UserHasRole($user, $role) {
		if(!($user instanceof UserRecord))
			$user = DB::Table('User')->Where('Username', EQ, $user)->Single();

		if(!($role instanceof UserRoleRecord))
			$role = DB::Table('UserRole')->Where('Name', EQ, $role)->Single();

		return $user->UserRoleXrefs()->Where('UserRoleID', EQ, $role->UserRoleID)->Exists();
	}


	/*
	 * IsAuth()
	 */
	public static function IsAuth() {
		return isset($_SESSION[authen_var]) && $_SESSION[authen_var] === true;
	}


	/*
	 * IsDev()
	 */
	public static function IsDev() {
		return self::IsAuth() && self::UserHasRole(self::$user, 'Developer');
	}


	/*
	 * IsAdmin()
	 */
	public static function IsAdmin() {
		return self::IsAuth() && self::UserHasRole(self::$user, 'Administrator');
	}


	/*
	 * GetIP()
	 */
	public static function GetIP() {
		self::SetIP();
		return IP::FromInteger(self::$ip);
	}


	/*
	 * SetIP()
	 */
	private static function SetIP($ip = false) {
		if (!$ip) {
			self::$ip = IP::ToInteger($_SERVER['REMOTE_ADDR']);
		} else if (strpos($ip, '.')) {
			self::$ip = IP::ToInteger($ip);
		} else if (is_numeric($ip)) {
			self::$ip = $ip;
		}
	}


	/*
	 * MatchIP()
	 */
	public static function MatchIP($compareTo = false) {
		//HACK
		return true;

		if (!$compareTo) {
			$compareTo = IP::ToInteger($_SERVER['REMOTE_ADDR']);
		} else if (!is_numeric($compareTo)) {
			$compareTo = IP::ToInteger($compareTo);
		}

		return array(self::GetIP(), $compareTo);

		return self::$ip === $compareTo;
	}


	/*
	 * GetPersistentID()
	 */
	public static function GetPersistentID($requireNew = false) {
		if ($requireNew || !isset(self::$persistentID)) {
			self::$persistentID = sha1(self::User()->Password . self::GetIP() . time());
		}
		return self::$persistentID;
	}


	/*
	 * GetSecret()
	 */
	public static function GetSecret($requireNew = false) {
		if (!self::$secret && $_SESSION['secret']) {
			self::$secret = $_SESSION['secret'];
		}

		if ($requireNew || !self::$secret || !self::MatchIP() || !self::ValidateSecret()) {
			session_regenerate_id();

			self::$sessionID = session_id();
			self::$secret = sha1(uniqid(mt_rand(), true) . session_id());
			self::$secretExpires = time() + 3600;
			self::SetIP();

			$_SESSION['secret'] = self::$secret;

			DB::Table('Secret')->NewRow(array(
				'SessionID'	=> self::$sessionID,
				'IPAddress'	=> self::$ip,
				'Secret'	=> self::$secret,
				'Expires'	=> self::$secretExpires
			))->Save();
		}

		return self::$secret;
	}


	/*
	 * ValidateSecret()
	 */
	public static function ValidateSecret($secret = false) {
		if (!$secret) {
			$secret = $_SESSION['secret'];
			if (!$secret) {
				fb('Auth::$secret = false');
				return false;
			}
		}

		fb('Secret');

		if (!DB::IsNull($secret = DB::Table('Secret')->Where('UUID', EQ, $secret)->OrderBy('Expires', DESC)->Single())) {
			return self::MatchIP($secret->IPAddress);
			fb('Got Secret from db');
			return $secret->Expires >= time() && $secret->SessionID == session_id() && self::MatchIP($secret->IPAddress);
		} else {
			fb('Didn\'t get secret');
			return false;
		}
	}


	/*
	 * DestroySecret()
	 */
	public static function DestroySecret($secret = false) {
		if (!$secret) {
			$secret = $_SESSION['secret'];
			if (!$secret) {
				return false;
			}
		}

		return DB::Table('Secret')->Where('UUID', EQ, $secret)->Delete() == 1;
	}


	/*
	 * MakeSalt()
	 */
	public static function MakeSalt() {
		return $_SESSION['ModAuth']['Salt'] = dechex(mt_rand() ^ hexdec(uniqid()));
	}


	/*
	 * GetSalt()
	 */
	public static function GetSalt() {
		return $_SESSION['ModAuth']['Salt'];
	}


	/*
	 * HttpDigestParse
	 * Appropriated from PHP.Net
	 */
	private static function HttpDigestParse($txt) {
		// protect against missing data
		$data = array();
		$needed_parts = array(
			'nonce'		=> 1,
			'nc'		=> 1,
			'cnonce'	=> 1,
			'qop'		=> 1,
			'username'	=> 1,
			'uri'		=> 1,
			'response'	=> 1
		);

		preg_match_all('@(\w+)=(?:([\'"])([^\2]+)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

		foreach ($matches as $m) {
			$data[$m[1]] = $m[3] ? $m[3] : $m[4];
			unset($needed_parts[$m[1]]);
		}

		return $needed_parts ? false : $data;
	}


	/*
	 * ErrorMessage()
	 */
	public static function LoginErrorMessage($code) {
		switch ($code) {
			case self::INVALID:		return 'Invalid username or password.';
			case self::TIMED_OUT:		return 'Your session has been timed out.';
			case self::INCOMPLETE:		return 'You omitted information crucial to your login.';
			case self::SECRET_EXPIRED:	return 'Your login session has expired.';
			case self::NO_CLEARTEXT:	return 'Clear-text passwords are disabled.';
		}
	}


	/*
	 * GetLoginResult()
	 */
	public static function GetLoginResult($clear = false) {
		$result = $_SESSION['login_result'];
		if ($clear) {
			$_SESSION['login_result'] = self::CLEAR;
		}
		return $result;
	}


	/*
	 * SetLoginResult()
	 */
	private static function SetLoginResult($value) {
		$_SESSION['login_result'] = $value;
	}


	/*
	 * PrepareLogin()
	 */
	public static function PrepareLogin() {
		global $config;

		JS::Load('/js/core/login.js');

		JSRPC::ExposeFunction('Auth::Login');

		JSRPC::ExposeVariable('Auth::secret', self::GetSecret());
		JSRPC::ExposeVariable('Auth::salt', self::MakeSalt());
	}

}


/*
 * login_logout_link()
 */
function login_logout_link($expand_login = false) {
	global $config;

	JSRPC::ExposeVariable('login_url', $config['login']['url']);

	if (Auth::IsAuth() === true) {
		// display logout link
		echo "<div id=\"floating_login_link\">";
		echo "logged in as {$_SESSION['username']}<br />";
		echo "<a id=\"log_in_out\" class=\"log_out\" href=\"{$config['login']['url']}?do=logout\">( log out )</a></div>\n";
	} else {
		//display standard login link
		JSRPC::ExposeFunction('Auth::Logout');
		echo "<div id=\"floating_login_link\"><a id=\"log_in_out\" class=\"log_in\" href=\"{$config[ 'login' ][ 'url' ]}\">( log in )</a></div>\n";

		if ($expand_login) {
			show_login_form($expand_login); //display enhanced login
		}
	}
}


/*
 * show_login_form()
 */
function show_login_form($floating = false, $remember = true, $forget = true) {
	global $config;

	$config['stylesheets']['login_fieldset'] = "#login fieldset { border: none; }";

	Auth::PrepareLogin();

	if (Auth::GetLoginResult() != Auth::CLEAR) {
		$msg = Auth::LoginErrorMessage(Auth::GetLoginResult(true));
	}

?>
	<div class="login_box"<? if($floating) echo " id=\"floating_login_box\"" ?>>
		<form id="login" action="<?=$config['login']['url']?>" method="post">
			<fieldset>
				<input type="hidden" name="do" value="login" />
				<table>
					<tr>
						<td colspan="2" align="center">
							<span id="login_error" class="error"><?= $msg ?></span>
						</td>
					</tr>
					<tr>
						<td align="right"><b>user id</b></td>
						<td><input type="text" id="login_user" name="user" value="<?= Auth::LastUsername()?>" /></td>
					</tr>
					<tr>
						<td align="right"><b>password</b></td>
						<td>
							<input type="password" id="login_pass" name="pass" value="" />
<?php	if ($remember): ?>
						</td>
					</tr>
					<tr>
						<td colspan="2" align="right">
							<b>remember me</b>
							<input type="checkbox" id="login_persist" name="persistent" value="true" />
<?php	endif;
	if (!$floating): ?>
						</td>
					</tr>
					<tr>
						<td colspan="2" align="right">
<?php 	endif;	?>
							&nbsp;&nbsp;<input type="submit" id="login_submit" name="login" value="Login" />
						</td>
					</tr>
<?php	if (!$floating && $forget): ?>
					<tr>
						<td align="center" colspan="2">
							<a href="password.php">Lost your password?</a>
						</td>
					</tr>
<?php	endif;	?>
				</table>
			</fieldset>
		</form>
	</div>
<?php
}
